/* GOOGLE */
(function() {
var f = document.getElementById('cse-search-box');
if (!f) {
f = document.getElementById('searchbox_demo');
}
if (f && f.q) {
var q = f.q;
var n = navigator;
var l = location;
if (n.platform == 'Win32') {
q.style.cssText = 'border: 1px solid #7e9db9; padding: 2px;';
}
var b = function() {
if (q.value == '') {
q.style.background = '#FFFFFF url(http:\x2F\x2Fwww.google.com\x2Fcse\x2Fintl\x2Fit\x2Fimages\x2Fgoogle_custom_search_watermark.gif) left no-repeat';
}
};
var f = function() {
q.style.background = '#ffffff';
};
q.onfocus = f;
q.onblur = b;
if (!/[&?]q=[^&]/.test(l.search)) {
b();
}
}
})();

/*
Script: Element.Measure.js
	Extends the Element native object to include methods useful in measuring dimensions.

	Element.measure / .expose methods by Daniel Steigerwald
	License: MIT-style license.
	Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz

	License:
		MIT-style license.

	Authors:
		Aaron Newton

*/

Element.implement({

	measure: function(fn){
		var vis = function(el) {
			return !!(!el || el.offsetHeight || el.offsetWidth);
		};
		if (vis(this)) return fn.apply(this);
		var parent = this.getParent(),
			toMeasure = [],
			restorers = [];
		while (!vis(parent) && parent != document.body) {
			toMeasure.push(parent.expose());
			parent = parent.getParent();
		}
		var restore = this.expose();
		var result = fn.apply(this);
		restore();
		toMeasure.each(function(restore){
			restore();
		});
		return result;
	},

	expose: function(){
		if (this.getStyle('display') != 'none') return $empty;
		var before = this.getStyles('display', 'position', 'visibility');
		return this.setStyles({
			display: 'block',
			position: 'absolute',
			visibility: 'hidden'
		}).setStyles.pass(before, this);
	},

	getDimensions: function(options){
		options = $merge({computeSize: false},options);
		var dim = {};
		var getSize = function(el, options){
			return (options.computeSize)?el.getComputedSize(options):el.getSize();
		};
		if (this.getStyle('display') == 'none'){
			dim = this.measure(function(){
				return getSize(this, options);
			});
		} else {
			try { //safari sometimes crashes here, so catch it
				dim = getSize(this, options);
			}catch(e){}
		}
		return $chk(dim.x) ? $extend(dim, {width: dim.x, height: dim.y}) : $extend(dim, {x: dim.width, y: dim.height});
	},

	getComputedSize: function(options){
		options = $merge({
			styles: ['padding','border'],
			plains: {
				height: ['top','bottom'],
				width: ['left','right']
			},
			mode: 'both'
		}, options);
		var size = {width: 0,height: 0};
		switch (options.mode){
			case 'vertical':
				delete size.width;
				delete options.plains.width;
				break;
			case 'horizontal':
				delete size.height;
				delete options.plains.height;
				break;
		}
		var getStyles = [];
		//this function might be useful in other places; perhaps it should be outside this function?
		$each(options.plains, function(plain, key){
			plain.each(function(edge){
				options.styles.each(function(style){
					getStyles.push((style == 'border') ? style + '-' + edge + '-' + 'width' : style + '-' + edge);
				});
			});
		});
		var styles = {};
		getStyles.each(function(style){ styles[style] = this.getComputedStyle(style); }, this);
		var subtracted = [];
		$each(options.plains, function(plain, key){ //keys: width, height, plains: ['left', 'right'], ['top','bottom']
			var capitalized = key.capitalize();
			size['total' + capitalized] = 0;
			size['computed' + capitalized] = 0;
			plain.each(function(edge){ //top, left, right, bottom
				size['computed' + edge.capitalize()] = 0;
				getStyles.each(function(style, i){ //padding, border, etc.
					//'padding-left'.test('left') size['totalWidth'] = size['width'] + [padding-left]
					if (style.test(edge)){
						styles[style] = styles[style].toInt() || 0; //styles['padding-left'] = 5;
						size['total' + capitalized] = size['total' + capitalized] + styles[style];
						size['computed' + edge.capitalize()] = size['computed' + edge.capitalize()] + styles[style];
					}
					//if width != width (so, padding-left, for instance), then subtract that from the total
					if (style.test(edge) && key != style &&
						(style.test('border') || style.test('padding')) && !subtracted.contains(style)){
						subtracted.push(style);
						size['computed' + capitalized] = size['computed' + capitalized]-styles[style];
					}
				});
			});
		});

		['Width', 'Height'].each(function(value){
			var lower = value.toLowerCase();
			if(!$chk(size[lower])) return;

			size[lower] = size[lower] + this['offset' + value] + size['computed' + value];
			size['total' + value] = size[lower] + size['total' + value];
			delete size['computed' + value];
		}, this);

		return $extend(styles, size);
	}

});

/*
Script: Element.Pin.js
	Extends the Element native object to include the pin method useful for fixed positioning for elements.

	License:
		MIT-style license.

	Authors:
		Aaron Newton
*/

(function(){
	var supportsPositionFixed = false;
	window.addEvent('domready', function(){
		var test = new Element('div').setStyles({
			position: 'fixed',
			top: 0,
			right: 0
		}).inject(document.body);
		supportsPositionFixed = (test.offsetTop === 0);
		test.dispose();
	});

	Element.implement({

		pin: function(enable){
			if (this.getStyle('display') == 'none') return null;

			var p;
			if (enable !== false){
				p = this.getPosition();
				if (!this.retrieve('pinned')){
					var pos = {
						top: p.y - window.getScroll().y,
						left: p.x - window.getScroll().x
					};
					if (supportsPositionFixed){
						this.setStyle('position', 'fixed').setStyles(pos);
					} else {
						this.store('pinnedByJS', true);
						this.setStyles({
							position: 'absolute',
							top: p.y,
							left: p.x
						});
						this.store('scrollFixer', (function(){
							if (this.retrieve('pinned'))
								this.setStyles({
									top: pos.top.toInt() + window.getScroll().y,
									left: pos.left.toInt() + window.getScroll().x
								});
						}).bind(this));
						window.addEvent('scroll', this.retrieve('scrollFixer'));
					}
					this.store('pinned', true);
				}
			} else {
				var op;
				if (!Browser.Engine.trident){
					if (this.getParent().getComputedStyle('position') != 'static') op = this.getParent();
					else op = this.getParent().getOffsetParent();
				}
				p = this.getPosition(op);
				this.store('pinned', false);
				var reposition;
				if (supportsPositionFixed && !this.retrieve('pinnedByJS')){
					reposition = {
						top: p.y + window.getScroll().y,
						left: p.x + window.getScroll().x
					};
				} else {
					this.store('pinnedByJS', false);
					window.removeEvent('scroll', this.retrieve('scrollFixer'));
					reposition = {
						top: p.y,
						left: p.x
					};
				}
				this.setStyles($merge(reposition, {position: 'absolute'}));
			}
			return this.addClass('isPinned');
		},

		unpin: function(){
			return this.pin(false).removeClass('isPinned');
		},

		togglepin: function(){
			this.pin(!this.retrieve('pinned'));
		}

	});

})();

/*
Script: Element.Position.js
	Extends the Element native object to include methods useful positioning elements relative to others.

	License:
		MIT-style license.

	Authors:
		Aaron Newton
*/

(function(){

var original = Element.prototype.position;

Element.implement({

	position: function(options){
		//call original position if the options are x/y values
		if (options && ($defined(options.x) || $defined(options.y))) return original ? original.apply(this, arguments) : this;
		$each(options||{}, function(v, k){ if (!$defined(v)) delete options[k]; });
		options = $merge({
			relativeTo: document.body,
			position: {
				x: 'center', //left, center, right
				y: 'center' //top, center, bottom
			},
			edge: false,
			offset: {x: 0, y: 0},
			returnPos: false,
			relFixedPosition: false,
			ignoreMargins: false,
			allowNegative: false
		}, options);
		//compute the offset of the parent positioned element if this element is in one
		var parentOffset = {x: 0, y: 0};
		var parentPositioned = false;
		/* dollar around getOffsetParent should not be necessary, but as it does not return
		 * a mootools extended element in IE, an error occurs on the call to expose. See:
		 * http://mootools.lighthouseapp.com/projects/2706/tickets/333-element-getoffsetparent-inconsistency-between-ie-and-other-browsers */
		var offsetParent = this.measure(function(){
			return $(this.getOffsetParent());
		});
		if (offsetParent && offsetParent != this.getDocument().body){
			parentOffset = offsetParent.measure(function(){
				return this.getPosition();
			});
			parentPositioned = true;
			options.offset.x = options.offset.x - parentOffset.x;
			options.offset.y = options.offset.y - parentOffset.y;
		}
		//upperRight, bottomRight, centerRight, upperLeft, bottomLeft, centerLeft
		//topRight, topLeft, centerTop, centerBottom, center
		var fixValue = function(option){
			if ($type(option) != 'string') return option;
			option = option.toLowerCase();
			var val = {};
			if (option.test('left')) val.x = 'left';
			else if (option.test('right')) val.x = 'right';
			else val.x = 'center';
			if (option.test('upper') || option.test('top')) val.y = 'top';
			else if (option.test('bottom')) val.y = 'bottom';
			else val.y = 'center';
			return val;
		};
		options.edge = fixValue(options.edge);
		options.position = fixValue(options.position);
		if (!options.edge){
			if (options.position.x == 'center' && options.position.y == 'center') options.edge = {x:'center', y:'center'};
			else options.edge = {x:'left', y:'top'};
		}

		this.setStyle('position', 'absolute');
		var rel = $(options.relativeTo) || document.body;
		var calc = rel == document.body ? window.getScroll() : rel.getPosition();
		var top = calc.y;
		var left = calc.x;

		if (Browser.Engine.trident){
			var scrolls = rel.getScrolls();
			top += scrolls.y;
			left += scrolls.x;
		}

		var dim = this.getDimensions({computeSize: true, styles:['padding', 'border','margin']});
		if (options.ignoreMargins){
			options.offset.x = options.offset.x - dim['margin-left'];
			options.offset.y = options.offset.y - dim['margin-top'];
		}
		var pos = {};
		var prefY = options.offset.y;
		var prefX = options.offset.x;
		var winSize = window.getSize();
		switch(options.position.x){
			case 'left':
				pos.x = left + prefX;
				break;
			case 'right':
				pos.x = left + prefX + rel.offsetWidth;
				break;
			default: //center
				pos.x = left + ((rel == document.body ? winSize.x : rel.offsetWidth)/2) + prefX;
				break;
		}
		switch(options.position.y){
			case 'top':
				pos.y = top + prefY;
				break;
			case 'bottom':
				pos.y = top + prefY + rel.offsetHeight;
				break;
			default: //center
				pos.y = top + ((rel == document.body ? winSize.y : rel.offsetHeight)/2) + prefY;
				break;
		}

		if (options.edge){
			var edgeOffset = {};

			switch(options.edge.x){
				case 'left':
					edgeOffset.x = 0;
					break;
				case 'right':
					edgeOffset.x = -dim.x-dim.computedRight-dim.computedLeft;
					break;
				default: //center
					edgeOffset.x = -(dim.x/2);
					break;
			}
			switch(options.edge.y){
				case 'top':
					edgeOffset.y = 0;
					break;
				case 'bottom':
					edgeOffset.y = -dim.y-dim.computedTop-dim.computedBottom;
					break;
				default: //center
					edgeOffset.y = -(dim.y/2);
					break;
			}
			pos.x = pos.x + edgeOffset.x;
			pos.y = pos.y + edgeOffset.y;
		}
		pos = {
			left: ((pos.x >= 0 || parentPositioned || options.allowNegative) ? pos.x : 0).toInt(),
			top: ((pos.y >= 0 || parentPositioned || options.allowNegative) ? pos.y : 0).toInt()
		};
		if (rel.getStyle('position') == 'fixed' || options.relFixedPosition){
			var winScroll = window.getScroll();
			pos.top = pos.top.toInt() + winScroll.y;
			pos.left = pos.left.toInt() + winScroll.x;
		}

		if (options.returnPos) return pos;
		else this.setStyles(pos);
		return this;
	}

});

})();

/*
Script: Element.Shortcuts.js
	Extends the Element native object to include some shortcut methods.

	License:
		MIT-style license.

	Authors:
		Aaron Newton

*/

Element.implement({

	isDisplayed: function(){
		return this.getStyle('display') != 'none';
	},

	toggle: function(){
		return this[this.isDisplayed() ? 'hide' : 'show']();
	},

	hide: function(){
		var d;
		try {
			//IE fails here if the element is not in the dom
			if ('none' != this.getStyle('display')) d = this.getStyle('display');
		} catch(e){}

		return this.store('originalDisplay', d || 'block').setStyle('display', 'none');
	},

	show: function(display){
		return this.setStyle('display', display || this.retrieve('originalDisplay') || 'block');
	},

	swapClass: function(remove, add){
		return this.removeClass(remove).addClass(add);
	}

});

/////////////////////////

//
// new SortingTable( 'my_table', {
// zebra: true, // Stripe the table, also on initialize
// details: false, // Has details every other row
// paginator: false, // Pass a paginator object
// onSorted: function(){}, // Callback to run after sort
// dont_sort_class: 'nosort', // Class name on th's that don't sort
// forward_sort_class: 'forward_sort', // Class applied to forward sort th's
// reverse_sort_class: 'reverse_sort' // Class applied to reverse sort th's
// });
//
// The above were the defaults. The regexes in load_conversions test a cell
// begin sorted for a match, then use that conversion for all elements on that
// column.
//
// Requires mootools Class, Array, Function, Element, Element.Selectors,
// Element.Event, and you should probably get Window.DomReady if you're smart.
//

var SortingTable = new Class({

  Implements: Options,

  options: {
    zebra: true,
    details: false,
    paginator: false,
    onSorted: $empty,
    dont_sort_class: 'nosort',
    forward_sort_class: 'forward_sort',
    reverse_sort_class: 'reverse_sort'
  },

  initialize: function( table, options ) {
    this.table = $(table);
    this.setOptions(options);

    this.tbody = this.table.getElement('tbody');
    if (this.options.zebra) {
      SortingTable.stripe_table(this.tbody.getChildren());
    }

    this.headers = this.table.getElement('thead').getElements('th');
    this.headers.each(function( header, index ) {
      if (header.hasClass( this.options.dont_sort_class )) { return }
      header.store( 'column', index )
      header.addEvent( 'mousedown', function(evt){
        this.sort_by_header( evt.target );
        if ( this.options.paginator) this.options.paginator.to_page( 1 );
      }.bind( this ) );
    }, this);

    this.load_conversions();
  },

  sort_by_header: function( header ){
    var rows = [];

    var before = this.tbody.getPrevious();
    this.tbody.dispose();

    var trs = this.tbody.getChildren();
    while ( row = trs.shift() ) {
      row = { row: row.dispose() };
      if ( this.options.details ) {
        row.detail = trs.shift().dispose();
      }
      rows.unshift( row );
    }

    if ( this.sort_column >= 0 &&
         this.sort_column == header.retrieve('column') ) {
      // They were pulled off in reverse
      if ( header.hasClass( this.options.reverse_sort_class ) ) {
        header.removeClass( this.options.reverse_sort_class );
        header.addClass( this.options.forward_sort_class );
      } else {
        header.removeClass( this.options.forward_sort_class );
        header.addClass( this.options.reverse_sort_class );
      }
    } else {
      this.headers.each(function(h){
        h.removeClass( this.options.forward_sort_class );
        h.removeClass( this.options.reverse_sort_class );
      }, this);
      this.sort_column = header.retrieve('column');
      if (header.retrieve('conversion_function')) {
        this.conversion_matcher = header.retrieve('conversion_matcher');
        this.conversion_function = header.retrieve('conversion_function');
      } else {
        this.conversion_function = false;
        rows.some(function(row){
          var to_match = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          if (to_match == '') return false;
          this.conversions.some(function(conversion){
            if (conversion.matcher.test( to_match )){
              this.conversion_matcher = conversion.matcher;
              this.conversion_function = conversion.conversion_function;
              return true;
            }
            return false;
          }, this);
          return !!(this.conversion_function);
        }, this);
        header.store('conversion_function', this.conversion_function );
        header.store('conversion_matcher', this.conversion_matcher );
      }
      header.addClass( this.options.forward_sort_class );
      rows.each(function(row){
        var compare_value = this.conversion_function( row );
        row.toString = function(){
          return compare_value;
        };
      }, this);
      rows.sort();
    }

    var index = 0;
    while ( row = rows.shift() ) {
      this.tbody.appendChild(row.row);
      if (row.detail) this.tbody.appendChild(row.detail);
      if ( this.options.zebra ) {
        row.row.className = row.row.className.replace( this.removeAltClassRe, '$1').clean();
        if (row.detail)
          row.detail.className = row.detail.className.replace( this.removeAltClassRe, '$1').clean();
        if (index % 2) {
          row.row.addClass( 'alt' );
          if (row.detail) row.detail.addClass( 'alt' );
        }
      }
      index++;
    }
   this.tbody.inject(before, 'after');
   this.options.onSorted();
  },

  load_conversions: function() {
    this.conversions = $A([
      // 1.7 M, 301 G, 34 K, 8 T or blank UMBACOS
      { matcher: /([0-9.]{1,8}).*([KMGT]{1})|/,
        conversion_function: function( row ) {
          var cello = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          var cell = this.conversion_matcher.exec( cello ); //alert(cello + " - " + cell[1] + " - " + cell[2]);
          if (!cell) { return '0' }
          if (cello=='') return '00000000';
          else if (!cell[2]) sort_val='1';
          else if (cell[2] == 'K') sort_val = '2';
          else if (cell[2] == 'M') sort_val = '3';
          else if (cell[2] == 'G') sort_val = '4';
          else if (cell[2] == 'T') sort_val = '5';
          else sort_val = '0';

          if (!cell[1]) cell[1]=cello;
          return sort_val.concat('00000000'.substr(0,6-cell[1].length).concat(cell[1]));
        }
      },
      // 1 day ago, 4 days ago, 38 years ago, 1 month ago
      { matcher: /(\d{1,2}) (.{3,6}) ago/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          cell = this.conversion_matcher.exec( cell );
          if (!cell) { return '0' }
          var sort_val;
          if (cell[2].indexOf('month') != -1) {
            sort_val = '1';
          } else if (cell[2].indexOf('year') != -1) {
            sort_val = '2';
          } else {
            sort_val = '0';
          }
          return sort_val.concat('00'.substr(0,2-cell[1].length).concat(cell[1]));
        }
      },
      // Currency
      { matcher: /((\d{1}\.\d{2}|\d{2}\.\d{2}|\d{3}\.\d{2}|\d{4}\.\d{2}|\d{5}\.\d{2}|\d{6}\.\d{2}))/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          cell = cell.replace(/[^\d]/g, "");
          return '00000000000000000000000000000000'.substr(0,32-cell.length).concat(cell);
        }
      },
      // YYYY-MM-DD, YYYY-m-d
      { matcher: /(\d{4})-(\d{1,2})-(\d{1,2})/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          cell = this.conversion_matcher.exec( cell );
          return cell[1]+
                 '00'.substr(0,2-cell[2].length).concat(cell[2])+
                 '00'.substr(0,2-cell[3].length).concat(cell[3]);
        }
      },
      // d/m/YYYY h:m:s UMBACOS
      { matcher: /(\d{1,2})\/(\d{1,2})\/(\d{4}).(\d{1,2})\:(\d{1,2})\:(\d{1,2})/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          cell = this.conversion_matcher.exec( cell );
          return cell[3]+
                 '00'.substr(0,2-cell[2].length).concat(cell[2])+
                 '00'.substr(0,2-cell[1].length).concat(cell[1])+
                 '00'.substr(0,2-cell[4].length).concat(cell[4])+
                 '00'.substr(0,2-cell[5].length).concat(cell[5])+
                 '00'.substr(0,2-cell[6].length).concat(cell[6]);
        }
      },
      // Numbers
      { matcher: /^\d+$/,
        conversion_function: function( row ) {
          var cell = $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
          return '00000000000000000000000000000000'.substr(0,32-cell.length).concat(cell);
        }
      },
      // Fallback
      { matcher: /.*/,
        conversion_function: function( row ) {
          return $(row.row.getElementsByTagName('td')[this.sort_column]).get('text');
        }
      }
    ]);
  }

});

SortingTable.removeAltClassRe = new RegExp('(^|\\s)alt(?:\\s|$)');
SortingTable.implement({ removeAltClassRe: SortingTable.removeAltClassRe });

SortingTable.stripe_table = function ( tr_elements ) {
  var counter = 0;
  tr_elements.each( function( tr ) {
    if ( !tr.hasClass('collapsed') ) counter++;
    tr.className = tr.className.replace( this.removeAltClassRe, '$1').clean();
    if (counter % 2) tr.addClass( 'alt' );
  });
}

var multipleOpenAccordion = new Class({
		Extends: Accordion,
		options: {
			allowMultipleOpen: true,
			display: 0,
			initialDisplayFx: false,
			onComplete: $empty,
			onActive: $empty,
			onBackground: $empty
		},
		initialize: function(acc,togglers,togglees,options){
			if (options.initialDisplayFx) this.options.initialDisplayFx=options.initialDisplayFx;
			if (options.display) this.options.display=options.display;
			if (options.onComplete) this.options.onComplete=options.onComplete;
			if (options.onActive) this.options.onActive=options.onActive;
			if (options.onBackground) this.options.onBackground=options.onBackground;
			this.parent(acc,togglers,togglees,this.options);
		},
		display: function(index, useFx){
			useFx = $pick(useFx, true);
			index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
			if ((this.timer && this.options.wait) || (index === this.previous && !this.options.alwaysHide)) return this;

			var obj = {};
			var el = this.elements[index];
			obj[index] = {};
			var hide = (el.offsetHeight > 0);
			this.fireEvent(hide ? 'onBackground' : 'onActive', [this.togglers[index], el]);
			for (var fx in this.effects) obj[index][fx] = hide ? 0 : el[this.effects[fx]];
			return useFx ? this.start(obj) : this.set(obj);
		},
		resize: function(index, useFx){
			useFx = $pick(useFx, true);
			index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
			if ((this.timer && this.options.wait) || (index === this.previous && !this.options.alwaysHide)) return this;

			var obj = {};
			var el = this.elements[index];
			if (!(el.offsetHeight > 0)) return;
			obj[index] = {};
			for (var fx in this.effects) obj[index][fx] = el[this.effects[fx]];
			return useFx ? this.start(obj) : this.set(obj);
		},
		isOpen: function(index){
			index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
			return (this.elements[index].offsetHeight > 0);
		}
	});

var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
function encode64(input) {
  var output = "";
  var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
  var i = 0;

  do {
	 chr1 = chr2 = chr3 = enc1 = enc2 = enc3 = enc4 = "";
	 chr1 = input.charCodeAt(i++);
	 chr2 = input.charCodeAt(i++);
	 chr3 = input.charCodeAt(i++);
	 enc1 = chr1 >> 2;
	 enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
	 enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
	 enc4 = chr3 & 63;
	 if (isNaN(chr2)) {
		enc3 = enc4 = 64;
	 } else if (isNaN(chr3)) {
		enc4 = 64;
	 }

	 output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
  } while (i < input.length);

  return output;
}

function decode64(input) {
  var output = "";
  var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
  var i = 0;

  // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
  input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

  do {
	 chr1 = chr2 = chr3 = enc1 = enc2 = enc3 = enc4 = "";
	 enc1 = keyStr.indexOf(input.charAt(i++));
	 enc2 = keyStr.indexOf(input.charAt(i++));
	 enc3 = keyStr.indexOf(input.charAt(i++));
	 enc4 = keyStr.indexOf(input.charAt(i++));
	 chr1 = (enc1 << 2) | (enc2 >> 4);
	 chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
	 chr3 = ((enc3 & 3) << 6) | enc4;
	 		 output += String.fromCharCode(chr1);
	 if (enc3 != 64) output += String.fromCharCode(chr2);
	 if (enc4 != 64) output += String.fromCharCode(chr3);
  } while (i < input.length);

  return output;
}
